package com.yxc.ejad.config;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.YearMonthDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.YearMonthSerializer;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.message.BasicHeader;
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
import org.apache.http.ssl.SSLContextBuilder;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import sun.misc.BASE64Decoder;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;

/**
 * ElasticSearch的配置
 *
 * @author: YXC
 * @time: 2023/9/25 16:05
 **/
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "spring.elasticsearch")
public class ElasticsearchConfig {

    /**
     * MD5密码加密开头
     */
    private static final String BASE64_PRE = "B/";
    /**
     * 地址，支持集群
     * 如果是es8之后的，要添加协议，写明https，否则默认为http
     */
    private String[] hosts;
    /**
     * 超时时间设为5分钟
     */
    private int timeOut = 5 * 60 * 1000;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     * 支持BASE64加密，需要使用B/开头
     */
    private String password;
    /**
     * 在kibana当中生成的apiKey
     */
    private String apiKey;
    /**
     * 连接协议，默认为http
     */
    private String protocol = "http";

    /**
     * 时间格式化
     */
    String localDateTimeFormat = "yyyy-MM-dd HH:mm:ss";
    String localDateFormat = "yyyy-MM-dd";
    String localTimeFormat = "HH:mm:ss";
    String yearMonth = "yyyy-MM";

    @Bean
    public ElasticsearchClient client() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException,
            IllegalAccessException {
        log.info("connecting elasticsearch(" + String.join(",", hosts) + ")");
        RestClient restClient = getRestClient();

        // Create the transport with a Jackson mapper
        ElasticsearchTransport transport = new RestClientTransport(
                restClient, new JacksonJsonpMapper(objectMapper()));

        // And create the API client
        ElasticsearchClient esClient = new ElasticsearchClient(transport);
        log.info("connect elasticsearch success");
        return esClient;
    }

    public ObjectMapper objectMapper() {
        ObjectMapper om = new ObjectMapper();
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        //序列化

        javaTimeModule.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(localDateTimeFormat)));

        javaTimeModule.addSerializer(LocalDate.class,
                new LocalDateSerializer(DateTimeFormatter.ofPattern(localDateFormat)));

        javaTimeModule.addSerializer(LocalTime.class,
                new LocalTimeSerializer(DateTimeFormatter.ofPattern(localTimeFormat)));

        javaTimeModule.addSerializer(YearMonth.class,
                new YearMonthSerializer(DateTimeFormatter.ofPattern(yearMonth)));
        //反序列化
        javaTimeModule.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(localDateTimeFormat)));
        javaTimeModule.addDeserializer(LocalDate.class,
                new LocalDateDeserializer(DateTimeFormatter.ofPattern(localDateFormat)));
        javaTimeModule.addDeserializer(LocalTime.class,
                new LocalTimeDeserializer(DateTimeFormatter.ofPattern(localTimeFormat)));
        javaTimeModule.addDeserializer(YearMonth.class,
                new YearMonthDeserializer(DateTimeFormatter.ofPattern(yearMonth)));
        om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        om.registerModule(javaTimeModule);
        return om;
    }

    private RestClient getRestClient() throws IllegalAccessException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
        HttpHost[] httpHosts = toHttpHost();

        // 设置SSL
        SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (x509Certificates, s) -> true).build();
        SSLIOSessionStrategy sessionStrategy = new SSLIOSessionStrategy(sslContext, NoopHostnameVerifier.INSTANCE);

        // 用户名密码认证
        final CredentialsProvider provider = new BasicCredentialsProvider();
        provider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(getUsername(), getPassword()));

        // 超时时间
        RequestConfig.Builder requestConfigBuilder = RequestConfig.custom()
                //超时时间5分钟
                .setConnectTimeout(timeOut)
                //这就是Socket超时时间设置
                .setSocketTimeout(timeOut)
                .setConnectionRequestTimeout(timeOut);

        // apiKey认证
        BasicHeader authorization = new BasicHeader("Authorization", "ApiKey " + getApiKey());

        RestClientBuilder builder = RestClient.builder(httpHosts);

        if (!useAuth()) {
            return builder.build();
        }


        if (useUsername()) {
            return builder
                    .setHttpClientConfigCallback(
                            b -> b.setSSLStrategy(sessionStrategy)
                                    .setDefaultCredentialsProvider(provider)
                                    .setDefaultRequestConfig(requestConfigBuilder.build())
                    )
                    .build();
        }
        if (useApiKey()) {
            return builder
                    .setHttpClientConfigCallback(b -> b.setSSLStrategy(sessionStrategy)
                            .setDefaultRequestConfig(requestConfigBuilder.build()))
                    .setDefaultHeaders(new Header[]{
                            authorization
                    })
                    .build();
        }
        throw new RuntimeException("Unknown Exception");
    }

    private HttpHost[] toHttpHost() {
        if (hosts == null || hosts.length == 0) {
            throw new RuntimeException("elasticsearch not config, please config elasticsearch.hosts");
        }
        HttpHost[] httpHosts = new HttpHost[hosts.length];
        HttpHost httpHost;
        try {
            for (int i = 0; i < hosts.length; i++) {
                String host = hosts[i];
                if (host.startsWith("http")) {
                    // 有协议
                    throw new IllegalAccessException("Host cannot contain protocols!");
                } else {
                    // 无协议
                    String[] strings = host.split(":");
                    // 默认为http协议
                    httpHost = new HttpHost(strings[0], Integer.parseInt(strings[1]), getProtocol());
                }
                httpHosts[i] = httpHost;
            }
        } catch (Exception e) {
            throw new RuntimeException("elasticsearch.hosts should be ip:port");
        }

        return httpHosts;
    }

    private Boolean useUsername() throws IllegalAccessException {
        return getUsername() != null && getPassword() != null && !"".equals(getUsername()) && !"".equals(getPassword());
    }

    private Boolean useApiKey() {
        return getApiKey() != null && !"".equals(getApiKey());
    }

    private Boolean useAuth() throws IllegalAccessException {
        return useUsername() || useApiKey();
    }

    public String getUsername() {
        return username == null ? "" : username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() throws IllegalAccessException {
        if (password == null) {
            return "";
        }
        // TODO: 2024/1/15 BASE64解密算法失败，重写 
        if (password.startsWith(BASE64_PRE)) {
            BASE64Decoder base64Decoder = new BASE64Decoder();
            try {
                return new String(base64Decoder.decodeBuffer(password));
            } catch (IOException e) {
                throw new IllegalAccessException("elasticsearch.password used wrong format!");
            }
        }
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String[] getHosts() {
        return hosts;
    }

    public void setHosts(String[] hosts) {
        this.hosts = hosts;
    }

    public String getApiKey() {
        return apiKey == null ? "" : apiKey;
    }

    public void setApiKey(String apiKey) {
        this.apiKey = apiKey;
    }

    public String getProtocol() {
        return protocol;
    }

    public void setProtocol(String protocol) {
        this.protocol = protocol;
    }
}
